/*
 * Copyright 2007 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.gwt.sockets.applets.java;

import netscape.javascript.JSObject;

import java.applet.Applet;
import java.net.Socket;
import java.io.*;

/**
 * The applet which provides socket functionality. To work correctly this applet must be signed.
 * When JS makes calls to this applet (for connecting), JVM checks if JS is signed and
 * grants corresponding permissions for thread from JS. Because JS is not always signed (so JS thread won't have
 * all needed permissions), JS thread will be used only to notify thread of applet. Notified thread will do all work:
 * connect and other operations which need permissions.
 *
 * @author Aleksey Lagoshin
 */
public class BinaryApplet extends Applet {

  /**
   * Action to do nothing.
   */
  private static final int ACT_NOTHING = -1;
  /**
   * Action to connect.
   */
  private static final int ACT_CONNECT = 0;
  /**
   * Action to shutdown this applet.
   */
  private static final int ACT_SHUTDOWN = 1;

  /**
   * What should this applet do after notifying.
   */
  private int currentAction = ACT_NOTHING;

  /**
   * Applet identifier.
   */
  private String id;

  /**
   * Remote host to connect.
   */
  private String host;
  /**
   * Port to connect.
   */
  private int port;

  /**
   * JavaScript Window object.
   */
  private JSObject window;

  /**
   * The socket object.
   */
  private Socket socket;
  /**
   * Input stream.
   */
  private DataInputStream in;
  /**
   * Output stream.
   */
  private DataOutputStream out;

  private boolean manuallyClosed = false;

  public void start() {
    System.out.println("Binary Sockets Applet is started");

    id = getParameter("id");

    window = JSObject.getWindow(this);
    window.call("javabin_" + id + "_onAppletLoad", new Object[]{this});

    // We need to free main thread, because some browsers won't work correctly
    new Thread(new Runnable() {
      public void run() {
        do {
          // Waiting for commands from JS
          synchronized (BinaryApplet.this) {
            currentAction = ACT_NOTHING;
            try {
              BinaryApplet.this.wait();
            }
            catch (InterruptedException e) {
              System.out.println("Binary Sockets Applet has been terminated by browser");
              currentAction = ACT_SHUTDOWN;
            }

            dispatchCommand();
          }
        } while (currentAction != ACT_SHUTDOWN);
        
        closeImpl();
        System.out.println("Binary Sockets Applet is finished");
      }
    }, "BinarySocketsThread").start();
  }

  /**
   * Dispatching commands from JS.
   */
  private void dispatchCommand() {
    switch (currentAction) {
      case(ACT_CONNECT):
        connect();
        break;
    }
  }

  /**
   * Connects to host:port and reads bytes from input stream in separate thread.
   */
  private void connect() {
    // Closing previous connection
    if (socket != null)
      closeImpl();
    new Thread(new Runnable() {
      public void run() {
        System.out.println("Connecting to " + host + ':' + port);
        try {
          socket = new Socket(host, port);

          CheckingStream checkingStream = new CheckingStream(socket.getInputStream(), new StreamHandler());
          in = new DataInputStream(checkingStream);
          out = new DataOutputStream(new BufferedOutputStream(socket.getOutputStream()));

          System.out.println("Connected");
          window.call("javabin_" + id + "_onConnect", null);

          checkingStream.startReading();
        }
        catch (SecurityException e) {
          System.out.println("Applet hasn't got all needed permissions:");
          e.printStackTrace();
          window.call("javabin_" + id + "_onSecurityError", new Object[] {e.getMessage()});
        }
        catch (IOException e) {
          System.out.println("Couldn't connect. Exception:");
          e.printStackTrace();
          window.call("javabin_" + id + "_onIOError", new Object[] {e.getMessage()});
        }
      }
    }, "ConnectingThread").start();
  }

  /**
   * Closes socket and input/output threads.
   */
  private void closeImpl() {
    System.out.println("Closing connection");

    try {
      if (in != null)
        in.close();
      if (out != null)
        out.close();
      if (socket != null) {
        socket.close();
        socket = null;
        manuallyClosed = true;
      }
    }
    catch (IOException e) {
      e.printStackTrace();
    }
  }

  // Interface for JavaScript

  /**
   * Interface method for JavaScript, tells this applet to connect to host:port.
   *
   * @param host the host name
   * @param port the port number
   */
  public void connect(String host, int port) {
    synchronized (this) {
      currentAction = ACT_CONNECT;
      manuallyClosed = false;
      this.host = host;
      this.port = port;
      notify();
    }
  }

  //TODO: It seems that we need to execute functions of JS in a new thread, if we don't want to halt main thread...
  //TODO: Maybe it's correct only for onData method... Need to think :)

  /**
   * Interface method for JavaScript, tells this applet to close connection.
   */
  public void close() {
    closeImpl();
  }

  public Object available() {
    try {
      return in.available();
    }
    catch (IOException e) {
      return "E1" + e.getMessage();
    }
  }

  public Object flush() {
    try {
      out.flush();
      return null;
    }
    catch (IOException e) {
      return "E1" + e.getMessage();
    }
  }

  public Object skip(int n) {
    try {
      return in.skip(n);
    }
    catch (IOException e) {
      return "E1" + e.getMessage();
    }
  }

  public Object readBoolean() {
    try {
      return in.readBoolean();
    }
    catch (EOFException e) {
      return "E2" + e.getMessage();
    }
    catch (IOException e) {
      return "E1" + e.getMessage();
    }
  }

  public Object readByte() {
    try {
      return in.readByte();
    }
    catch (EOFException e) {
      return "E2" + e.getMessage();
    }
    catch (IOException e) {
      return "E1" + e.getMessage();
    }
  }

  public Object readBytes(int length) {
    byte[] bytes = new byte[length];
    try {
      in.read(bytes);
    }
    catch (IOException e) {
      return "E1" + e.getMessage();
    }

    StringBuffer result = new StringBuffer(length + 1);
    result.append("O");
    for (int i = 0; i < length; i++)
      result.append(new Character((char) bytes[i]));

    return result;
  }

  public Object readDouble() {
    try {
      return in.readDouble();
    }
    catch (EOFException e) {
      return "E2" + e.getMessage();
    }
    catch (IOException e) {
      return "E1" + e.getMessage();
    }
  }

  public Object readFloat() {
    try {
      return in.readFloat();
    }
    catch (EOFException e) {
      return "E2" + e.getMessage();
    }
    catch (IOException e) {
      return "E1" + e.getMessage();
    }
  }

  public Object readInt() {
    try {
      return in.readInt();
    }
    catch (EOFException e) {
      return "E2" + e.getMessage();
    }
    catch (IOException e) {
      return "E1" + e.getMessage();
    }
  }

  public Object readShort() {
    try {
      return in.readShort();
    }
    catch (EOFException e) {
      return "E2" + e.getMessage();
    }
    catch (IOException e) {
      return "E1" + e.getMessage();
    }
  }

  public Object readUnsignedByte() {
    try {
      return in.readUnsignedByte();
    }
    catch (EOFException e) {
      return "E2" + e.getMessage();
    }
    catch (IOException e) {
      return "E1" + e.getMessage();
    }
  }

  public Object readUnsignedInt() {
    try {
      long ch1 = in.read();
      long ch2 = in.read();
      long ch3 = in.read();
      long ch4 = in.read();
      if ((ch1 | ch2 | ch3 | ch4) < 0)
        return "E2";

      return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4 << 0));
    }
    catch (IOException e) {
      return "E1" + e.getMessage();
    }
  }

  public Object readUnsignedShort() {
    try {
      return in.readUnsignedShort();
    }
    catch (EOFException e) {
      return "E2" + e.getMessage();
    }
    catch (IOException e) {
      return "E1" + e.getMessage();
    }
  }

  public Object readUTF() {
    try {
      return "O" + in.readUTF();
    }
    catch (EOFException e) {
      return "E2" + e.getMessage();
    }
    catch (IOException e) {
      return "E1" + e.getMessage();
    }
  }

  public Object readString(int length, String charSet) {
    try {
      byte[] bytes = new byte[length];
      if (in.read(bytes) != length)
        return "E2";

      //TODO: Unknown charSet exception?
      return "O" + new String(bytes, charSet);
    }
    catch (IOException e) {
      return "E1" + e.getMessage();
    }
  }

  public Object writeBoolean(boolean value) {
    try {
      out.writeBoolean(value);
      return null;
    }
    catch (IOException e) {
      return "E1" + e.getMessage();
    }
  }

  public Object writeByte(int value) {
    try {
      out.writeByte(value);
      return null;
    }
    catch (IOException e) {
      return "E1" + e.getMessage();
    }
  }

  public Object writeBytes(String bytesStr) {
    try {
      out.writeBytes(bytesStr);
      return null;
    }
    catch (IOException e) {
      return "E1" + e.getMessage();
    }
  }

  public Object writeDouble(double value) {
    try {
      out.writeDouble(value);
      return null;
    }
    catch (IOException e) {
      return "E1" + e.getMessage();
    }
  }

  public Object writeFloat(float value) {
    try {
      out.writeFloat(value);
      return null;
    }
    catch (IOException e) {
      return "E1" + e.getMessage();
    }
  }

  public Object writeInt(int value) {
    try {
      out.writeInt(value);
      return null;
    }
    catch (IOException e) {
      return "E1" + e.getMessage();
    }
  }

  public Object writeShort(short value) {
    try {
      out.writeShort(value);
      return null;
    }
    catch (IOException e) {
      return "E1" + e.getMessage();
    }
  }

  public Object writeUnsignedInt(long value) {
    try {
      out.writeInt((int) value);
      return null;
    }
    catch (IOException e) {
      return "E1" + e.getMessage();
    }
  }

  public Object writeUTF(String value) {
    try {
      out.writeUTF(value);
      return null;
    }
    catch (IOException e) {
      return "E1" + e.getMessage();
    }
  }

  public Object writeString(String value, String charSet) {
    try {
      out.write(value.getBytes(charSet));
      return null;
    }
    catch (IOException e) {
      return "E1" + e.getMessage();
    }
  }

  private class StreamHandler implements StreamListener {

      public void onData(InputStream in, int loadedBytes) {
        window.call("javabin_" + id + "_onData", new Object[]{new Integer(loadedBytes)});
      }

      public void onClose(InputStream in) {
        if (!manuallyClosed)
          window.call("javabin_" + id + "_onClose", null);
      }

  }

}
